﻿using LitJson;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Devil;

namespace GameToolkit.ContentProvider
{
    /// <summary>
    /// 表数据合并
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IMergeTableData
    {
        void MergeFrom(TableBase data);
    }

    public interface ITable : IEnumerable, System.IDisposable
    {
        event System.Action<ITable> OnLoaded;
        int Count { get; }
        object FirstData { get; }
        bool ContainsID(int id);
        object GetValue(int id);
        object GetValueAt(int index);
        System.Type DataType { get; }
        //T GetCallback<T>() where T : class, ITableCallback;
    }

    public abstract class TableSet : ITable
    {
        public event System.Action<ITable> OnLoaded;

        protected readonly static object _lock = new object();
        //protected readonly static HashSet<ITable> sGlobalTables = new HashSet<ITable>();
        //public static IEnumerator<ITable> GetGlobalTables()
        //{
        //    return sGlobalTables.GetEnumerator();
        //}
        protected readonly object mLock = new object();
        protected HashSet<int> mMerged = new HashSet<int>();
        int mVersion;
        uint mLoadRef;
        bool mDisposed;
        public int Version => mVersion;
        public bool IsLoading => mLoadRef > 0;
        public abstract int Count { get; }
        public abstract object FirstData { get; }
        public abstract System.Type DataType { get; }
        public abstract int GetSerializationID();
        public abstract IEnumerator GetEnumerator();
        public abstract bool ContainsID(int id);
        public abstract object GetValue(int id);
        public abstract object GetValueAt(int index);
        internal abstract TableSet NewTableSet();
        internal abstract void LoadDatas(string table, JsonData arr);
        internal abstract void InitDatas(IList datas);
        internal void StartLoading()
        {
            lock (mLock)
            {
                if (mDisposed)
                    throw new System.Exception($"The table<{DataType.Name}> was disposed.");
                mLoadRef++;
                if (mLoadRef == 1)
                {
                    mVersion++;
                }
            }
        }
        internal void FinishLoading()
        {
            lock (mLock)
            {
                if (mLoadRef == 0)
                    throw new System.Exception("Table's Loading was finished.");
                mLoadRef--;
                if (mLoadRef == 0)
                {
                    var first = FirstData as TableBase.IOnSubmitTable;
                    if (first != null)
                        first.OnSubmit(this);
                    OnLoaded?.Invoke(this);
                }
            }
        }

        internal bool JoinTable(string tablename)
        {
            var id = HashTable(this, tablename);
            lock (mLock)
            {
                return mMerged.Add(id);
            }
        }

        internal static void LoadTo(string tablename, string text, TableSet tab)
        {
            if (!string.IsNullOrEmpty(text) && tab != null)
            {
                JsonData arr = JsonMapper.ToObject(text);
                if (arr != null)
                    tab.LoadDatas(tablename, arr);
            }
        }

        public static int HashTable(TableSet table, string tablename)
        {
            var id1 = table.DataType.FullName.IgnoreCaseToHash();
            var id2 = tablename.IgnoreCaseToHash();
            return MathExt.Hash(id1, id2);
        }

        internal static void MergeTo(TableSet tables, TableSet mergeTo)
        {
            if (tables == null || tables.Count == 0 || mergeTo == null)
                return;
            if (tables.DataType != mergeTo.DataType)
                throw new System.Exception("Can't merge tables between different data types.");
            lock (mergeTo.mLock)
            {
                mergeTo.StartLoading();
                lock (tables.mLock)
                {
                    foreach (var tab in tables.mMerged)
                    {
                        mergeTo.mMerged.Add(tab);
                    }
                    List<TableBase> list = new List<TableBase>(mergeTo.Count + tables.Count);
                    int a = 0;
                    int b = 0;
                    TableBase data = null;
                    while (a < mergeTo.Count || b < tables.Count)
                    {
                        var ta = a < mergeTo.Count ? mergeTo.GetValueAt(a) as TableBase : null;
                        var tb = b < tables.Count ? tables.GetValueAt(b) as TableBase : null;
                        if (ta == null && tb == null)
                            break;
                        if (ta != null && tb != null)
                        {
                            if (tb.DataId < ta.DataId)
                            {
                                list.Add(tb);
                                b++;
                            }
                            else if (ta.DataId < tb.DataId)
                            {
                                list.Add(ta);
                                a++;
                            }
                            else
                            {
                                if (tb is IMergeTableData merge)
                                    merge.MergeFrom(ta);
                                list.Add(tb);
                                b++;
                                a++;
                            }
                        }
                        else if (ta == null)
                        {
                            list.Add(tb);
                            data = tb;
                            b++;
                        }
                        else
                        {
                            list.Add(ta);
                            data = ta;
                            a++;
                        }
                    }
                    mergeTo.InitDatas(list);
                    //mergeTo.m_Datas = list.ToArray();
                }
                mergeTo.FinishLoading();
            }
        }

        public void Dispose()
        {
            lock (mLock)
            {
                if (mDisposed)
                    return;
                mDisposed = true;
            }
            OnDispose();
        }

        protected abstract void OnDispose();
    }

    // 数据集合
    public class TableSet<T> : TableSet where T : TableBase, new()
    {
        public struct DataReference : System.IEquatable<DataReference>, IEnumerator<T>
        {
            TableSet<T> table;
            int index;
            int ver;

            public static implicit operator T(DataReference dt)
            {
                return dt.Current;
            }

            internal DataReference(TableSet<T> table, int index)
            {
                this.table = table;
                this.index = index;
                this.ver = table.Version;
            }

            public bool IsNull
            {
                get
                {
                    if (table == null || table.Version != ver || table.m_Datas == null || table.IsLoading)
                        return true;
                    else
                        return index < 0 || index >= table.m_Datas.Length;
                }
            }

            public T Current
            {
                get
                {
                    if (table == null)
                        return null;
                    if (table.IsLoading)
                        throw new System.InvalidOperationException($"TableSet<{typeof(T).Name}> was still loading, can't get data right now.");
                    var datas = table.m_Datas;
                    if (ver != table.Version)
                        throw new System.Exception($"TableSet<{typeof(T).Name}>.DataReference is obsolute");
                    var len = datas == null ? 0 : datas.Length;
                    return index >= 0 && index < len ? datas[index] : null;
                }
            }

            public DataReference Previours
            {
                get
                {
                    if (table == null || index < 1 || ver != table.Version)
                        return default;
                    DataReference dt;
                    dt.ver = this.ver;
                    dt.index = this.index - 1;
                    dt.table = this.table;
                    return dt;
                }
            }

            public DataReference Next
            {
                get
                {
                    if (table == null || ver != table.Version)
                        return default;
                    DataReference dt;
                    dt.ver = this.ver;
                    dt.index = this.index + 1;
                    dt.table = this.table;
                    return dt;
                }
            }

            public bool Equals(DataReference other)
            {
                return this.ver == other.ver && this.index == other.index && this.table == other.table;
            }

            object IEnumerator.Current => this.Current;

            bool IEnumerator.MoveNext()
            {
                if(table != null && table.Version == ver && table.m_Datas != null)
                {
                    index++;
                    return index < table.m_Datas.Length;
                }
                else
                {
                    return false;
                }
            }

            void IEnumerator.Reset()
            {
                index = -1;
            }

            public void Dispose()
            {
                index = 0;
                table = null;
            }
        }

#if UNITY_EDITOR
        public bool IsEditorOnly { get; private set; }
#endif

        //static object sLock = new object();
        private T[] m_Datas;
        public string TableName { get; private set; }
        int mSerializationId;
        //public ITableCallback TableCallback { get; set; }
        public override object FirstData { get { return m_Datas == null || m_Datas.Length == 0 ? null : m_Datas[0]; } }

        public override int GetSerializationID()
        {
            lock (mLock)
            {
                if (mSerializationId == 0)
                {
                    var t = new T();
                    mSerializationId = t.SerializationID;
                    if (t is System.IDisposable dispose)
                        dispose.Dispose();
                    if (mSerializationId == 0)
                        mSerializationId = typeof(T).GetHashCode();
                }
            }
            return mSerializationId;
        }

        public override System.Type DataType { get { return typeof(T); } }

        public TableSet(bool editorOnly = false)
        {
#if UNITY_EDITOR
            IsEditorOnly = editorOnly;
#endif
        }

        public override bool ContainsID(int id)
        {
            var inst = FindData(id);
            return !inst.IsNull;
        }

        internal override TableSet NewTableSet()
        {
            return new TableSet<T>();
        }

        /// <summary>
        /// 最接近 id 的数据索引,
        /// fromLeft == true, 即表示小于等于 id 的最接近的数据索引吗，否则表示大于等于 id 的最接近数据
        /// </summary>
        /// <param name="id"></param>
        /// <param name="fromLeft"></param>
        /// <returns></returns>
        public DataReference FindDataOfClosestTo(int id, bool fromLeft)
        {
            lock (mLock)
            {
                if (IsLoading)
                    throw new System.InvalidOperationException(string.Format("Table<{0}> was still loading, can't find data right now.", DataType.Name));
                if (m_Datas == null)
                    return default;
                int l = 0;
                int r = m_Datas.Length - 1;
                int c;
                int cs;
                int ret = -1;
                while (l <= r)
                {
                    c = (l + r) >> 1;
                    cs = m_Datas[c].DataId;
                    if (cs == id)
                        return new DataReference(this, c);
                    else if (cs > id)
                        r = c - 1;
                    else
                        l = c + 1;
                    if (fromLeft ^ (cs > id))
                        ret = c;
                }
                return ret == -1 ? default : new DataReference(this, ret);
            }
        }

        /// <summary>
        /// 数据索引
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public DataReference FindData(int id)
        {
            lock (mLock)
            {
                if (IsLoading)
                    throw new System.InvalidOperationException(string.Format("Table<{0}> was still loading, can't get data right now.", DataType.Name));
                if (m_Datas == null)
                    return default;
                int l = 0;
                int r = m_Datas.Length - 1;
                int c;
                int cs;
                while (l <= r)
                {
                    c = (l + r) >> 1;
                    cs = m_Datas[c].DataId;// compareResult(list[c]);
                    if (cs == id)
                        return new DataReference(this, c);
                    else if (cs > id)
                        r = c - 1;
                    else
                        l = c + 1;
                }
            }
            return default;
        }

        public override object GetValue(int id)
        {
            var inst = FindData(id);
            return inst.Current;
        }

        public override object GetValueAt(int index)
        {
            var datas = m_Datas;
            if (datas != null && index >= 0 && index < datas.Length)
                return datas[index];
            else
                return null;
        }

        public T this[int id]
        {
            get
            {
                var index = FindData(id);
                return index.Current;
            }
        }

        public override IEnumerator GetEnumerator()
        {
            return new DataReference(this, -1);
        }

        public override int Count
        {
            get
            {
                return m_Datas == null ? 0 : m_Datas.Length;
            }
        }

        public T[] AsArray()
        {
            if (m_Datas == null)
                m_Datas = new T[0];
            return m_Datas;
        }

        internal override void InitDatas(IList datas)
        {
            StartLoading();
            try
            {
                var old = m_Datas == null || m_Datas.Length == 0 ? null : m_Datas[0];
                if (old is TableBase.IOnSubmitTable submit)
                    submit.OnDispose(this);
                var len = datas == null ? 0 : datas.Count;
                if (m_Datas == null || m_Datas.Length != len)
                    m_Datas = new T[len];
                for (int i = 0; i < len; i++)
                {
                    m_Datas[i] = (T)datas[i];
                }
            }
            catch (System.Exception e)
            {
                Debug.LogException(e);
            }
            FinishLoading();
        }

        internal override void LoadDatas(string table, JsonData arr)
        {
            StartLoading();
            lock (mLock)
            {
                mMerged.Clear();
                mMerged.Add(HashTable(this, table));
            }
            try
            {
                TableName = table;
                var old = m_Datas != null && m_Datas.Length > 0 ? m_Datas[0] : null;
                if (old is TableBase.IOnSubmitTable submit)
                    submit.OnDispose(this);
                if (m_Datas == null || m_Datas.Length != arr.Count)
                    m_Datas = new T[arr.Count];
                T data = null;
                for (int i = 0; i < arr.Count; i++)
                {
                    data = new T();
                    data.Init(arr[i]);
                    m_Datas[i] = data;
                }
            }
            catch (System.Exception e)
            {
                Debug.LogException(e);
                Debug.LogErrorFormat("parse \"{0}\" as <{1}> error", table, typeof(T).Name);
            }
            FinishLoading();
        }

        protected override void OnDispose()
        {
            var old = m_Datas == null || m_Datas.Length == 0 ? null : m_Datas[0];
            if (old is TableBase.IOnSubmitTable submit)
                submit.OnDispose(this);
            m_Datas = null;
            mMerged.Clear();
        }

        private static TableSet<T> s_Instance;
        public static TableSet<T> Instance
        {
            get
            {
                lock (_lock)
                {
                    if (s_Instance == null)
                    {
                        s_Instance = new TableSet<T>();
                        //sGlobalTables.Add(s_Instance);
                    }
                }
                return s_Instance;
            }
        }

        public static T Value(int id)
        {
            return s_Instance == null ? null : s_Instance[id];
        }

        public static void Release()
        {
            TableSet<T> inst;
            lock (_lock)
            {
                inst = s_Instance;
                if (s_Instance != null)
                {
                    //sGlobalTables.Remove(s_Instance);
                    s_Instance = null;
                }
            }
            if (inst != null)
                inst.InitDatas(null);
        }

        public static TableSet<T> LoadAsNew(string tablename, string text)
        {
            TableSet<T> inst = new TableSet<T>();
            LoadTo(tablename, text, inst);
            return inst;
        }

        public static TableSet<T> Load(string tablename, string text)
        {
            TableSet<T> inst = Instance;
            LoadTo(tablename, text, inst);
            return inst;
        }

        public static TableSet<T> Merge(string tablename, string text)
        {
            TableSet<T> inst = LoadAsNew(tablename, text);
            if (s_Instance == null)
            {
                s_Instance = inst;
                //sGlobalTables.Add(inst);
            }
            else
            {
                MergeTo(inst, s_Instance);
                inst.InitDatas(null);
            }
            return s_Instance;
        }

    }
}